package com.rayworks.library;

import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.Typeface;
import android.support.annotation.NonNull;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.AppCompatTextView;
import android.text.Html;
import android.text.InputType;
import android.text.Layout;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextPaint;
import android.text.TextUtils;
import android.text.method.LinkMovementMethod;
import android.text.style.ClickableSpan;
import android.text.style.ForegroundColorSpan;
import android.text.style.StyleSpan;
import android.text.style.UnderlineSpan;
import android.util.AttributeSet;
import android.util.Log;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;

import com.google.android.flexbox.FlexboxLayout;
import com.rayworks.library.util.Utils;

import java.util.HashMap;
import java.util.Map;

/**
 * Created by seanzhou on 11/5/15.
 * <p>
 * The customized textview provides a way to select text from input window to fill up inner blanks.
 * </p>
 */
public class GapFillTextView extends AppCompatTextView {

    private static final String TAG = "`GapFillTextView`";

    public static final int BLANK_TYPE_TEXT = 1;
    public static final int BLANK_TYPE_NUMBER = 2;
    public static final int BLANK_TYPE_SELECTIONS = 3;

    public static final String BLANK_CHAR = " ";
    private static final String BLANK_TAG = "____";
    /**
     * 原始的字符
     */
    private String[] texts;
    /**
     * 当前点的的空白的位置
     */
    private int clickedBlankPosition;
    /**
     * 当前点的的空白的位置暂存
     */
    private int clickedBlankPositionCache;
    /**
     * 总共的空白数目
     */
    private int totalBlankCount = 0;
    /**
     * 各个填空的类型
     */
    private Map<Integer, Integer> blankTypeMap = new HashMap<>();
    /**
     * 各个填空的标题
     */
    private Map<Integer, String> blankTitleMap = new HashMap<>();
    /**
     * 各个填空的提示
     */
    private Map<Integer, String> blankHintMap = new HashMap<>();
    /**
     * 各个填空的选择项
     */
    private Map<Integer, String[]> blankSelectionsMap = new HashMap<>();
    /**
     * 各个填空填入的字符
     */
    private HashMap<Integer, String> filledTextMap = new HashMap<>();
    /**
     * 原始的字符
     */
    private String rawStr;
    /***
     * 是否为浏览模式, 浏览模式,
     */
    private boolean reviewMode;
    /**
     * 回调
     */
    private Listener listener = null;
    /**
     * the line index corresponding to the specified vertical position
     **/
    private int lineIndexHit;
    //编辑的兑换框的一些View
    private EditText edtContent;
    private Button btnNext;
    private TextView txvTitle;
    private ViewGroup panelButtons;
    private FlexboxLayout fblButtons;
    private Dialog alertDialog;
    //一下是一些按键的字符
    private String confirmButtonText = "确定";
    private String clearButtonText = "清空";
    private String nextButtonText = "下一项";

    /**
     * 监听器
     */
    public interface Listener {
        void onContentSubmit(GapFillTextView view, int index, String content);
    }

    public GapFillTextView(Context context) {
        this(context, null);
    }

    public GapFillTextView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public GapFillTextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.GapFillTextView);
        if (ta != null) {
            CharSequence cs = ta.getText(R.styleable.GapFillTextView_raw_text);
            if (cs != null) {
                String rawText = cs.toString();
                setRawText(rawText);
            }
            ta.recycle();
        }
        init();
    }

    @Override
    protected void onDetachedFromWindow() {
        if (alertDialog != null && alertDialog.isShowing()) {
            alertDialog.dismiss();
        }
        super.onDetachedFromWindow();
    }

    private void init() {
        setMovementMethod(new LinkTouchMovementMethod());
    }

    ///////////////////////////////////////////////////////////////////////////
    // 公开的方法
    ///////////////////////////////////////////////////////////////////////////

    public boolean isReviewMode() {
        return reviewMode;
    }

    public void setReviewMode(boolean reviewMode) {
        this.reviewMode = reviewMode;
    }

    public void setBlankHintMap(Map<Integer, String> blankHintMap) {
        this.blankHintMap = blankHintMap;
    }

    public void setConfirmButtonText(String confirmButtonText) {
        this.confirmButtonText = confirmButtonText;
    }

    public void setClearButtonText(String clearButtonText) {
        this.clearButtonText = clearButtonText;
    }

    public void setBlankTypeMap(Map<Integer, Integer> blankTypeMap) {
        this.blankTypeMap = blankTypeMap;
    }

    public void setBlankTitleMap(Map<Integer, String> blankTitleMap) {
        this.blankTitleMap = blankTitleMap;
    }

    public Map<Integer, String[]> getBlankSelectionsMap() {
        return blankSelectionsMap;
    }

    public void setBlankSelectionsMap(Map<Integer, String[]> blankSelectionsMap) {
        this.blankSelectionsMap = blankSelectionsMap;
    }

    public void setRawText(String rawText) throws IllegalArgumentException {
        if (TextUtils.isEmpty(rawText)) {
            throw new IllegalArgumentException("Main text in Wring Material can't be empty.");
        }
        normalizeInput(rawText);
        setFormattedText(false, null);
    }

    public void setListener(Listener listener) {
        this.listener = listener;
    }

    /***
     * 获取格式的文本
     */
    public String getFormattedFullText() {
        String originalTxt = this.rawStr;
        String[] strs = originalTxt.split("\\{\\}");

        if (strs.length > 1) {
            StringBuilder builder = new StringBuilder(strs[0]);
            for (int i = 1; i < strs.length; i++) {
                builder.append('{');
                String filledText = filledTextMap.get(i - 1);
                builder.append(filledText == null ? "" : filledText);
                builder.append('}');
                builder.append(strs[i]);
            }

            return builder.toString();
        }

        throw new IllegalStateException("Text is not filled up completely");
    }

    /***
     * 获取纯文本
     */
    public String getPlainFullText() {
        String originalTxt = this.rawStr;
        String[] strs = originalTxt.split("\\{\\}");
        if (strs.length > 1) {
            StringBuilder builder = new StringBuilder(strs[0]);
            for (int i = 1; i < strs.length; i++) {
                String filledText = filledTextMap.get(i - 1);
                builder.append(filledText == null ? "" : filledText);
                builder.append(strs[i]);
            }

            return builder.toString();
        }
        throw new IllegalStateException("Text is not filled up completely");
    }

    /***
     * 清除填入的字符
     */
    public void reset() {
        filledTextMap.clear();
        clickedBlankPositionCache = clickedBlankPosition;
        clickedBlankPosition = -1;
        setFormattedText(true, null);
        clickedBlankPosition = clickedBlankPositionCache;
    }

    /**
     * 设置填入的字符
     *
     * @param filledTexts 填入的字符
     * @param refresh     是否刷新显示
     */
    public void setFilledTexts(HashMap<Integer, String> filledTexts, boolean refresh) {
        this.filledTextMap = filledTexts;
        if (refresh) {
            clickedBlankPositionCache = clickedBlankPosition;
            clickedBlankPosition = -1;
            setFormattedText(true, null);
            clickedBlankPosition = clickedBlankPositionCache;
        }
    }

    /**
     * 增加填入的字符
     *
     * @param texts   填入的字符
     * @param refresh 是否刷新显示
     */
    public void addFilledTexts(HashMap<Integer, String> texts, boolean refresh) {
        if (texts != null && texts.size() > 0) {
            for (Integer position : texts.keySet()) {
                this.filledTextMap.put(position, texts.get(position));
            }
        }
        if (refresh) {
            clickedBlankPositionCache = clickedBlankPosition;
            clickedBlankPosition = -1;
            setFormattedText(true, null);
            clickedBlankPosition = clickedBlankPositionCache;
        }
    }

    /**
     * 设置填入的字符
     *
     * @param position 填入的位置
     * @param text     字符
     * @param refresh  是否刷新显示
     */
    public void setFilledText(Integer position, String text, boolean refresh) {
        filledTextMap.put(position, text);
        if (refresh) {
            clickedBlankPositionCache = clickedBlankPosition;
            clickedBlankPosition = -1;
            setFormattedText(true, null);
            clickedBlankPosition = clickedBlankPositionCache;
        }
    }

    ///////////////////////////////////////////////////////////////////////////
    // 内部的方法
    ///////////////////////////////////////////////////////////////////////////

    /***
     * Refines the input text, adds blanks as extra segment to make sure there will be N {*} blocks
     * and N+1 filtered segments
     *
     * @param text
     */
    private void normalizeInput(String text) {
        if (text.endsWith("}")) {
            text += BLANK_CHAR;
            Log.d(TAG, ">>> normalizing Input in the end with " + BLANK_CHAR);
        }

        if (text.startsWith("{")) {
            text = BLANK_CHAR + text;
            Log.d(TAG, ">>> normalizing Input in the beginning with " + BLANK_CHAR);
        }

        this.rawStr = text;
    }

    /***
     * Applies the formatted text
     *
     * @param hitDetected whether a span gets hit.
     * @param source      new source string for clicked span. It's valid when {@code hitDetected} is true.
     */
    private void setFormattedText(boolean hitDetected, String source) {
        String formattedStr = Utils.getHtmlTextWithMarkups(this.rawStr);

        SpannableStringBuilder stringBuilder = setupSpansForMultipleSelection(hitDetected, source, formattedStr);

        setText(stringBuilder);

    }

    @NonNull
    private SpannableStringBuilder setupSpansForMultipleSelection(boolean hitDetected, String source, String formattedStr) {
        String regex = "\\{(.*?)\\}"; // filter by all the {}, {texts} style strings
        texts = formattedStr.split(regex);

        SpannableStringBuilder ssb = new SpannableStringBuilder();

        int lastMatchedTextPosition = 0;
        totalBlankCount = 0;
        for (int i = 0; i < texts.length; i++) {

            boolean hitIndex = false;

            String placeholder = "";

            // the hit area detection
            // we start counting clickable span from the position 1 of filtered text.
            int spanIndex = i - 1;
            if (hitDetected && i > 0) {
                if (spanIndex == clickedBlankPosition) {
                    hitIndex = true;
                    String selectedSource = source;
                    placeholder = selectedSource;
                    if (TextUtils.isEmpty(source)) {
                        filledTextMap.remove(clickedBlankPosition);
                    } else {
                        filledTextMap.put(clickedBlankPosition, placeholder);
                    }
                } else if (filledTextMap.containsKey(spanIndex)) {
                    hitIndex = true;
                    placeholder = filledTextMap.get(spanIndex); // concat the historic texts
                } else {
                    placeholder = "";
                }
            }

            String str = texts[i];

            // add the separator underline span
            if (i > 0 && i <= texts.length - 1) {

                // check the initial values
                if (TextUtils.isEmpty(placeholder)) {
                    // find the start index after the end index of previous text to avoid locating
                    // the wrong position.
                    int preTextStartPos = formattedStr.indexOf(texts[i - 1], lastMatchedTextPosition);

                    lastMatchedTextPosition = preTextStartPos + texts[i - 1].length();

                    // move forward to the most closest position to the new segment if any
                    lastMatchedTextPosition = formattedStr.indexOf("}", lastMatchedTextPosition) + 1;

                    // find the end index after the start position
                    int currentTextStartPos = formattedStr.indexOf(texts[i], lastMatchedTextPosition);

                    placeholder = formattedStr.substring(preTextStartPos + texts[i - 1].length() + 1, currentTextStartPos - 1);

                    if (TextUtils.isEmpty(placeholder)) {
                        // only the "{}" found, replaced with the underline
                        placeholder = BLANK_TAG;
                    } else {
                        // apply the html format for input content
                        placeholder = Html.fromHtml(placeholder).toString();
                    }
                } else {
                    // cached text detected, move forward the index of matched text.
                    lastMatchedTextPosition += texts[i - 1].length();
                    // one step ahead of the filled blank
                    lastMatchedTextPosition = formattedStr.indexOf("}", lastMatchedTextPosition) + 1;
                }

                addClickableSpanArea(ssb, placeholder, spanIndex, hitIndex);

                ssb.append(Html.fromHtml(str));
            } else {
                ssb.append(Html.fromHtml(str));
            }
        }

        return ssb;
    }

    /***
     * Appends the clickable span with formatted text
     *
     * @param stringBuilder        span string builder [IN/OUT param]
     * @param placeholder          content for the span area
     * @param indexOfClickableSpan clickable span index
     * @param hitIndex             whether current span is clicked
     */
    private void addClickableSpanArea(SpannableStringBuilder stringBuilder,
                                      String placeholder,
                                      int indexOfClickableSpan,
                                      boolean hitIndex) {

        SpannableString spanUnderString = new SpannableString(placeholder);

        TagClickableSpan urlSpan = new TagClickableSpan(indexOfClickableSpan);
        spanUnderString.setSpan(urlSpan, 0, placeholder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);

        if (hitIndex) {
            ForegroundColorSpan colorSpan;
            colorSpan = new ForegroundColorSpan(Color.BLACK);
            spanUnderString.setSpan(colorSpan, 0, placeholder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);

            StyleSpan span = new StyleSpan(Typeface.BOLD);
            spanUnderString.setSpan(span, 0, placeholder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
            spanUnderString.setSpan(new UnderlineSpan(), 0, placeholder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        }
        stringBuilder.append(spanUnderString);
        totalBlankCount++;
        // append horizental space
        stringBuilder.append(" ");
    }

    private void showInputSourceView() {
        int scrolledTxtVerticalOffset = getScrollY();
        Log.d(TAG,
              ">>> Scrolled Y " + scrolledTxtVerticalOffset + " top: " + getTop() + " bottom: " + getBottom() + " height: " + getHeight());
        showEditorView();
    }

    private void showEditorView() {
        if (alertDialog == null) {
            final View view = LayoutInflater.from(getContext()).inflate(R.layout.rtv_dialog_gapfill_form, null);
            final Button confirmBtn = (Button) view.findViewById(R.id.button_ok);
            final Button clearBtn = (Button) view.findViewById(R.id.button_clear);
            btnNext = (Button) view.findViewById(R.id.button_next);
            txvTitle = view.findViewById(R.id.rtvTxvTitle);
            fblButtons = view.findViewById(R.id.rtvFblButtons);
            panelButtons = view.findViewById(R.id.rtvPanelButtons);
            confirmBtn.setText(confirmButtonText);
            clearBtn.setText(clearButtonText);
            btnNext.setText(nextButtonText);
            edtContent = (EditText) view.findViewById(R.id.rtvEdtContent);
            confirmBtn.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    String newTxt = edtContent.getText().toString().trim();
                    if (listener != null) {
                        listener.onContentSubmit(GapFillTextView.this, clickedBlankPosition, newTxt);
                    }
                    populateTextView(newTxt);
                    alertDialog.dismiss();
                }
            });

            clearBtn.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    edtContent.setText("");
                    populateTextView("");
                }
            });
            btnNext.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    String newTxt = edtContent.getText().toString().trim();
                    if (listener != null) {
                        listener.onContentSubmit(GapFillTextView.this, clickedBlankPosition, newTxt);
                    }
                    populateTextView(newTxt);
                    clickedBlankPosition++;
                    showInputSourceView();
                }
            });

            if (alertDialog == null) {
                alertDialog = new AlertDialog.Builder(getContext()).setView(view).create();

                // disable dim background
                alertDialog.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);

                alertDialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
                    @Override
                    public void onDismiss(DialogInterface dialog) {
                        GapFillTextView.this.requestFocus();
                        GapFillTextView.this.postDelayed(new Runnable() {
                            @Override
                            public void run() {
                                InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
                                imm.hideSoftInputFromWindow(GapFillTextView.this.getWindowToken(), 0);
                            }
                        }, 100);
                    }
                });
            }
        }
        String editboxText = "";
        if (filledTextMap.containsKey(clickedBlankPosition)) {
            editboxText = filledTextMap.get(clickedBlankPosition);
        }
        edtContent.setText(editboxText);
        int inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE;
        boolean showButtons = false;
        if (blankTypeMap != null && blankTypeMap.containsKey(clickedBlankPosition)) {
            int blankType = blankTypeMap.get(clickedBlankPosition);
            switch (blankType) {
                case BLANK_TYPE_NUMBER:
                    inputType = InputType.TYPE_CLASS_NUMBER | InputType.TYPE_TEXT_FLAG_MULTI_LINE;
                    break;
                case BLANK_TYPE_SELECTIONS:
                    showButtons = true;
                    fblButtons.removeAllViews();
                    if (blankSelectionsMap != null && blankSelectionsMap.containsKey(clickedBlankPosition)) {
                        String[] selections = blankSelectionsMap.get(clickedBlankPosition);
                        if (selections != null && selections.length != 0) {
                            Context context = fblButtons.getContext();
                            OnClickListener listener = new OnClickListener() {
                                @Override
                                public void onClick(View v) {
                                    edtContent.setText(((Button) v).getText());
                                    edtContent.setSelection(edtContent.length());
                                }
                            };
                            for (String selection : selections) {
                                Button button = new Button(new ContextThemeWrapper(context, R.style.RtvSelectionButtonsStyle), null, 0);
                                button.setText(selection);
                                button.setOnClickListener(listener);
                                fblButtons.addView(button);
                            }
                        }
                    }
                    break;
            }
        }
        edtContent.setInputType(inputType);
        panelButtons.setVisibility(showButtons ? VISIBLE : GONE);
        if (blankHintMap != null && blankHintMap.containsKey(clickedBlankPosition)) {
            String hint = blankHintMap.get(clickedBlankPosition);
            edtContent.setHint(hint);
        } else {
            edtContent.setHint(null);
        }
        if (blankTitleMap != null && blankTitleMap.containsKey(clickedBlankPosition)) {
            txvTitle.setText(blankTitleMap.get(clickedBlankPosition));
            txvTitle.setVisibility(VISIBLE);
        } else {
            txvTitle.setVisibility(GONE);
        }
        btnNext.setVisibility(clickedBlankPosition < (totalBlankCount - 1) ? VISIBLE : INVISIBLE);
        edtContent.setSelection(editboxText.length());
        alertDialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
        alertDialog.show();
    }

    private void populateTextView(String source) {
        setFormattedText(true, source);
    }

    /***
     * Gets the hint line index
     *
     * @return index if found, otherwise -1;
     */
    public int getHintLineIndex() {
        SpannableString spannableString = new SpannableString(getText());
        ClickableSpan[] correctionSpans = spannableString.getSpans(0, spannableString.length(), ClickableSpan.class);

        Layout layout = getLayout();

        int lineCount = layout.getLineCount();

        int targetLineIndex = -1;
        if (correctionSpans.length > 0) {
            for (int j = 0; j < correctionSpans.length; j++) {
                ClickableSpan span = correctionSpans[j];

                int lineStartPos, lineEndPos;

                for (int i = 0; i < lineCount; i++) {
                    lineStartPos = layout.getLineStart(i);
                    lineEndPos = layout.getLineEnd(i);

                    int spanTxtIndex = spannableString.getSpanStart(span);

                    boolean atLineRange = lineStartPos <= spanTxtIndex && lineEndPos > spanTxtIndex;
                    boolean notFilled = filledTextMap.get(j) == null;
                    if (atLineRange && notFilled) {
                        targetLineIndex = i;
                        Log.d(TAG, ">>> target tip line index: " + targetLineIndex + ", span index: " + j);
                        break;
                    }
                }
                if (targetLineIndex >= 0) {
                    break;
                }
            }
        }
        return targetLineIndex;
    }

    public class TagClickableSpan extends ClickableSpan {

        private final int index;

        public TagClickableSpan(int index) {
            this.index = index;
        }

        @Override
        public void onClick(View widget) {
            clickedBlankPosition = index;

            if (!isReviewMode()) showInputSourceView();
        }

        @Override
        public void updateDrawState(TextPaint ds) {
            // get rid of the underline
            ds.setUnderlineText(false);

            // align the color of original text
            ds.setColor(GapFillTextView.this.getTextColors().getDefaultColor());
        }
    }

    // http://stackoverflow.com/questions/20856105/change-the-text-color-of-a-single-clickablespan-when-pressed-without-affecting-o#
    class LinkTouchMovementMethod extends LinkMovementMethod {

        @Override
        public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {
            int action = event.getAction();
            if (action == MotionEvent.ACTION_UP) {
                int x = (int) event.getX();
                int y = (int) event.getY();

                x -= widget.getTotalPaddingLeft();
                y -= widget.getTotalPaddingTop();

                x += widget.getScrollX();
                y += widget.getScrollY();

                Layout layout = widget.getLayout();
                lineIndexHit = layout.getLineForVertical(y);
                int off = layout.getOffsetForHorizontal(lineIndexHit, x);

                TagClickableSpan clickedSpan = null;
                TagClickableSpan[] spans = buffer.getSpans(off, off, TagClickableSpan.class);

                int xLineStart = layout.getLineStart(lineIndexHit);
                Log.d(TAG, ">>> text index Line : " + xLineStart);

                if (spans.length > 0) {
                    clickedSpan = spans[0];
                    int spanTxtIndex = buffer.getSpanStart(clickedSpan);
                    Log.d(TAG, ">>> text index Span : " + spanTxtIndex);

                    // measure the text from the start of current line to the span start pos
                    String fullText = widget.getText().toString();

                    String linePreTxt = "";
                    int spanEndIndex = buffer.getSpanEnd(clickedSpan);
                    if (xLineStart > spanTxtIndex) {
                        // already filled in with a long text ?!
                        linePreTxt = fullText.substring(spanTxtIndex, spanEndIndex);
                    } else {
                        linePreTxt = fullText.substring(xLineStart, spanTxtIndex);
                    }
//                    clickedSpanString = fullText.substring(spanTxtIndex, spanEndIndex);
//
//                    offsetXClickedSpan = (int) widget.getPaint().measureText(linePreTxt) + widget.getPaddingLeft();
                }

                Log.d(TAG, ">>> Line location LineNo: " + lineIndexHit + " offset Hor: " + off);
            }
            return super.onTouchEvent(widget, buffer, event);
        }
    }

}
